﻿// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------

namespace BSDockerVolumePlugin
{
    using System;
    using System.Collections.Generic;
    using System.Xml.Linq;
    using System.Threading;
    using BSDockerVolumePlugin.Models;
    using System.Fabric;
    using System.Diagnostics;
    using System.IO;
    using Newtonsoft.Json;
    using System.Net;
    using System.Net.Sockets;
    using System.Runtime.InteropServices;
    using Microsoft.ServiceFabric.Diagnostics.Tracing;
    using System.Security.AccessControl;
    using System.Security.Principal;
  
    public class VolumeMap : IDisposable
    {
        // TODO: Make volumesMetadata single file instead of multi files
        private const string volumesMetadata = "volumesMetadata";
        private const string servicesMetadata = "servicesMetadata";
        public const string servicesMetadataFilenameWithExt = "servicesMetadata.xml";
        private const string mountPointsDirectory = "mountPoints";
        private const int defaultProcessRuntimeMs = 30000; // 30 seconds

        // TODO: Coarse lock - improve.
        private readonly ReaderWriterLockSlim volumeRWLock;
        private readonly Dictionary<string, VolumeEntry> volumeMappings; // <VolumeName, VolumeEntry>
        private readonly ReaderWriterLockSlim serviceRWLock;
        private readonly XElement serviceMappings;  // <ServicePartitionId, ServicePort> key is case-insensitive
        private readonly ServiceContext serviceContext;
        private readonly string mountPointBase;

        // Define the PInvoke definitions required for LU provisioning.
#if DotNetCoreClrLinux
        [DllImport("libSFBDCtlLib.so", SetLastError=true)]
#else        
        [DllImport("SFBDCtlLib.dll", SetLastError=true)]
#endif        
        private static extern IntPtr ConnectToDevice(ref uint lastError);

#if DotNetCoreClrLinux
        [DllImport("libSFBDCtlLib.so", EntryPoint="CloseLinuxHandle", SetLastError=true)]
#else
        [DllImport("Kernel32.dll", SetLastError=true)]
#endif
        private static extern bool CloseHandle(IntPtr handle);

#if DotNetCoreClrLinux
        [DllImport("libSFBDCtlLib.so", SetLastError=true)]
#else        
        [DllImport("SFBDCtlLib.dll", SetLastError=true)]
#endif        
        private static extern bool ProvisionLUN(IntPtr hDevice, [MarshalAs(UnmanagedType.LPWStr)] string pLUID, [MarshalAs(UnmanagedType.LPWStr)] string pSize, [MarshalAs(UnmanagedType.LPWStr)] string pSizeType, [MarshalAs(UnmanagedType.LPWStr)] string FileSystem, [MarshalAs(UnmanagedType.LPWStr)] string pMountPoint, uint dwServicePort);

#if DotNetCoreClrLinux
        [DllImport("libc.so.6", EntryPoint="umount")]
        private static extern int umount(string target);
#endif

        static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);

        public VolumeMap(ServiceContext serviceContext)
        {
            this.volumeRWLock = new ReaderWriterLockSlim();
            this.volumeMappings = new Dictionary<string, VolumeEntry>();
            this.serviceRWLock = new ReaderWriterLockSlim();
            this.serviceMappings = new XElement(servicesMetadata);
            this.serviceContext = serviceContext;
            this.mountPointBase = Path.Combine(this.serviceContext.CodePackageActivationContext.WorkDirectory, mountPointsDirectory);
            Utilities.EnsureFolder(this.mountPointBase);

            this.Initialize();
        }

        private string GetTraceId(string functionName)
        {
            if (TraceWriter.writeToConsole)
            {
                // Return an ID generated by us if we are writing traces to console
                return "[" + Guid.NewGuid().ToString() + "][" + functionName + "]";
            }
            else
            {
                return this.serviceContext.TraceId;
            }
        }

        private string GetFormattedPartitionId(string servicePartitionId)
        {
            return String.Format("PartId-{0}", servicePartitionId.ToLower());
        }

        public bool UpdateServiceMappings(string servicePartitionID, int port)
        {
            string traceId = GetTraceId("VolumeMap.UpdateServiceMappings");

            this.serviceRWLock.EnterWriteLock();
            try
            {
                string servicesMetadataFileName = Path.Combine(this.serviceContext.CodePackageActivationContext.WorkDirectory, servicesMetadata, servicesMetadataFilenameWithExt);

                string formattedServicePartitionID = GetFormattedPartitionId(servicePartitionID);

                serviceMappings.SetElementValue(formattedServicePartitionID, port.ToString());

                // Read from map to verify
                XElement registeredElement = serviceMappings.Element(formattedServicePartitionID);

                bool fStatus = false;
                
                if (registeredElement != null)
                {
                    int portNum = 0;
                    if (Int32.TryParse(registeredElement.Value, out portNum))
                    {
                        if (portNum == port)
                        {
                            serviceMappings.Save(servicesMetadataFileName);
                            TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, String.Format("Successfully updated service map for servicePartitionID:{0}, port:{1}", servicePartitionID, port));
                            fStatus = true;
                        }
                        else
                        {
                            TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, String.Format("Port mismatch {0}:{1} for servicePartitionID {2} during updating map", port, portNum, servicePartitionID));
                        }
                    }
                    else
                    {
                        if (string.IsNullOrEmpty(registeredElement.Value))
                        {
                            TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, String.Format("Unable to parse port for servicePartitionID {0} from null during updating map", servicePartitionID));
                        }
                        else
                        {
                            TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, String.Format("Unable to parse port for servicePartitionID {0} from {1} during updating map", servicePartitionID, registeredElement.Value));
                        }
                    }
                }
                else
                {
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, String.Format("Unable to find servicePartitionID:{0} during updating map", servicePartitionID));
                }
                return fStatus;
            }
            catch (Exception e)
            {
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, "Unexpected exception: "+e.ToString());
                return false;
            }
            finally
            {
                this.serviceRWLock.ExitWriteLock();
            }

        }

        public Response CreateVolume(VolumeCreateRequest request, string traceId)
        {
            if (this.VolumeExists(request.Name))
            {
                string errorMesg = String.Format("Volume ${0} already exists", request.Name);
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMesg );
                return new Response(errorMesg);
            }

            this.volumeRWLock.EnterWriteLock();
            try
            {
                // Use volume name as LUID for now, the length limitation will be checked at InvalidCreateRequest
                // TODO: Create by using System.Guid.NewGuid().ToString() and save to globle reliable map to handle the failover case from Node1 to Node2
                request.Opts[Constants.LUID] = request.Name;

                string errorMessage = "";

                if (InvalidCreateRequest(request, ref errorMessage))
                {
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                    return new Response(errorMessage);
                }

                string metadataFilename = Path.Combine(this.serviceContext.CodePackageActivationContext.WorkDirectory, volumesMetadata, request.Name);
                if (!this.WriteMetadataFile(metadataFilename, request))
                {
                    errorMessage = String.Format("Unable to persist metadata for volume {0}", request.Name);
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                    return new Response(errorMessage);
                }

                this.volumeMappings[request.Name] = new VolumeEntry()
                {
                    VolumeOptions   = request.Opts,
                    ServicePartitionID = request.Opts[Constants.ServicePartitionID],
                    LUID            = request.Opts[Constants.LUID],
                    SizeDisk        = request.Opts[Constants.SizeDisk],
#if DotNetCoreClrLinux
                    // This need to be fixed in application manifest generator.
                    // To pass EXT4 in case of Linux.
                    FileSystem       = "EXT4",
#else
                    FileSystem      = request.Opts[Constants.FileSystem],
#endif
                    Mountpoint      = GetMountpoint(request.Name)
                };

                return new Response();
            }
            catch(Exception ex)
            {
                string errorMessage = String.Format("Volume {0} could not be created due to an exception: {1}", request.Name, ex.Message);
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                return new Response(errorMessage);    
            }
            finally
            {
                this.volumeRWLock.ExitWriteLock();
            }
        }

        //Linux Specific PInvoke methods.
        [DllImport("libc.so.6", EntryPoint="chmod")]
        private static extern int Chmod(string path, int mode);

        private bool SetMountpointRWAccessForUsers(string mountPointPath, string traceId)
        {
            string status = String.Empty;
            bool fSetACL = false;

#if DotNetCoreClrLinux
            try
            {
                const int defaultFilePermission = 0x1ed;
                Chmod(mountPointPath, defaultFilePermission);
                fSetACL = true;
            }
            catch
            {
                status = "Chmod failed";
            }
#else
            try
            {
                // Create a new DirectoryInfo object.
                DirectoryInfo dInfo = new DirectoryInfo(mountPointPath);

                // Get a DirectorySecurity object that represents the 
                // current security settings.
                DirectorySecurity dSecurity = FileSystemAclExtensions.GetAccessControl(dInfo);

                var allUsers = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);

                // Add the FileSystemAccessRule to the security settings. 
                bool fModified = false;
                dSecurity.ModifyAccessRule(AccessControlModification.Add, new FileSystemAccessRule(allUsers,
                                                                FileSystemRights.Modify,
                                                                InheritanceFlags.ContainerInherit|InheritanceFlags.ObjectInherit,
                                                                PropagationFlags.None, // Make it applied to This Folder, subfolders and files
                                                                AccessControlType.Allow), out fModified);

                // Set the new access settings.
                if (fModified)
                {
                    FileSystemAclExtensions.SetAccessControl(dInfo, dSecurity);
                    fSetACL = true;
                }
                else
                {
                    status = String.Format("Failed to change ACL of mountpoint {0}.", mountPointPath);
                }
            }
            catch(Exception ex)
            {
                status = String.Format("Failed to change ACL of mountpoint {0} due to exception: {1}.", mountPointPath, ex.Message);
            }
#endif

            if (!String.IsNullOrEmpty(status))
            {
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, status);
            }

            return fSetACL;
        }

        public Response MountVolume(VolumeMountRequest request, string traceId)
        {
            if (!this.VolumeExists(request.Name))
            {
                string errorMessage = String.Format("Attempt to mount volume {0} that does not exist.", request.Name);
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                return new Response(errorMessage);
            }

            this.volumeRWLock.EnterWriteLock();
            try
            {
                if (!volumeMappings.ContainsKey(request.Name))
                {
                    string errorMessage = String.Format("Mapping for volume {0} was not found.", request.Name);
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                    return new Response(errorMessage);
                }

                VolumeEntry volumeEntry = this.volumeMappings[request.Name];

                string volumeDetails = String.Format("Volume:{0}, servicePartitionId:{1}, LUID:{2}, sizeDisk:{3}, fileSystem:{4}", request.Name, volumeEntry.ServicePartitionID, volumeEntry.LUID, volumeEntry.SizeDisk, volumeEntry.FileSystem);

                TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, String.Format("Processing mount request: {0}", volumeDetails));

                bool fVolumeProvisioned = false;
                if (!System.IO.Directory.Exists(volumeEntry.Mountpoint))
                {
                    // The folder created here is which volume will be mounted
                    System.IO.Directory.CreateDirectory(volumeEntry.Mountpoint);

                    // Give built-in users access to the folder
                    if (SetMountpointRWAccessForUsers(volumeEntry.Mountpoint, traceId))
                    {
                        // Provision the volume
                        TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, String.Format("Provisioning volume: {0}", volumeDetails));
                        fVolumeProvisioned = this.ProvisionLU(request.Name, volumeEntry, traceId);
                    }

                    // If volume provisioning fails, delete the mountpoint folder
                    if (!fVolumeProvisioned)
                    {
                        System.IO.Directory.Delete(volumeEntry.Mountpoint);
                    }
                }
                else
                {
#if DotNetCoreClrLinux
                    TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, "Attempting Umount before provisioning volume." );
                    var returnValue = umount(volumeEntry.Mountpoint);
                    TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, "Umount returned " + returnValue);
#endif
                    // Connect to BlockStore Native Service and send ProvisionLU cmd
                    TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, String.Format("Provisioning existing mountpoint for volume: {0}", volumeDetails));
                    fVolumeProvisioned = this.ProvisionLU(request.Name, volumeEntry, traceId);
                }

                if (!fVolumeProvisioned)
                {
                    string errorMessage = String.Format("Unable to provision volume: {0}", volumeDetails);
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);    
                    return new Response(errorMessage);
                }
                else
                {
                    TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, String.Format("Successfully provisioned volume: {0}", volumeDetails));  
                }

                volumeEntry.MountIDs.Add(request.ID);
                ++volumeEntry.NumberOfMounts;
#if DotNetCoreClrLinux
                Interlocked.Increment(ref BSDockerVolumePlugin.SynchronizedKestrelCommunicationListener.TotalMountedVolumesOnNode);
#endif

                return new VolumeMountResponse(volumeEntry.Mountpoint);
            }
            catch(Exception ex)
            {
                string errorMesg = String.Format("Unable to mount volume {0} due to exception: {1}", request.Name, ex.Message);
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMesg); 

                return new Response(errorMesg);   
            }
            finally
            {
                this.volumeRWLock.ExitWriteLock();
            }
        }

        public Response UnmountVolume(VolumeUnmountRequest request, string traceId)
        {
            if (!this.VolumeExists(request.Name))
            {
                string errorMessage = String.Format("Attempt to unmount volume {0} that does not exist.", request.Name);
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                return new Response(errorMessage);
            }

            this.volumeRWLock.EnterWriteLock();
            try
            {
                if (!volumeMappings.ContainsKey(request.Name))
                {
                    string errorMessage = String.Format("Mapping for volume {0} was not found.", request.Name);
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                    return new Response(errorMessage);
                }

                if (string.IsNullOrEmpty(this.volumeMappings[request.Name].Mountpoint) ||
                        !this.volumeMappings[request.Name].MountIDs.Contains(request.ID))
                {
                    // We are not tracking this volume mount.
                    return new Response();
                }

                this.volumeMappings[request.Name].MountIDs.Remove(request.ID);
                --this.volumeMappings[request.Name].NumberOfMounts;
#if DotNetCoreClrLinux
                Interlocked.Decrement(ref BSDockerVolumePlugin.SynchronizedKestrelCommunicationListener.TotalMountedVolumesOnNode);

                if (this.volumeMappings[request.Name].NumberOfMounts == 0)
                {
                    TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, "Attempting umount of " + this.volumeMappings[request.Name].Mountpoint);
                    var returnValue = umount(this.volumeMappings[request.Name].Mountpoint);
                    TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, "Umount returned" + returnValue);
                }
#endif
                return new Response();
            }
            finally
            {
                this.volumeRWLock.ExitWriteLock();
            }

        }

        public Response ListVolumes()
        {
            this.volumeRWLock.EnterReadLock();
            try
            {
                var volumesListResponse = new VolumeListResponse();
                foreach (var item in this.volumeMappings)
                {
                    volumesListResponse.Volumes.Add(new VolumeMountDescription()
                            {
                            Name = item.Key,
                            Mountpoint = item.Value.Mountpoint
                            });
                }

                return volumesListResponse;
            }
            finally
            {
                this.volumeRWLock.ExitReadLock();
            }
        }

        public Response GetVolume(VolumeName request, string traceId)
        {
            this.volumeRWLock.EnterReadLock();
            try
            {
                if (!volumeMappings.ContainsKey(request.Name))
                {
                    string errorMessage = String.Format("Mapping for volume {0} was not found.", request.Name);
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                    return new Response(errorMessage);
                }

                return new VolumeGetResponse(
                        new VolumeMountDescription()
                        {
                        Name = request.Name,
                        Mountpoint = volumeMappings[request.Name].Mountpoint
                        });
            }
            finally
            {
                this.volumeRWLock.ExitReadLock();
            }
        }

        public Response GetVolumeMountPoint(VolumeName request, string traceId)
        {
            this.volumeRWLock.EnterReadLock();
            try
            {
                if (!volumeMappings.ContainsKey(request.Name))
                {
                    string errorMessage = String.Format("Mapping for volume {0} was not found.", request.Name);
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                    return new Response(errorMessage);
                }

                return new VolumeMountResponse(this.volumeMappings[request.Name].Mountpoint);
            }
            finally
            {
                this.volumeRWLock.ExitReadLock();
            }
        }

        public Response RemoveVolume(VolumeName request, string traceId)
        {
            if (!this.VolumeExists(request.Name))
            {
                string errorMessage = String.Format("Attempt to remove volume {0} that does not exist.", request.Name);
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                return new Response(errorMessage);
            }

            this.volumeRWLock.EnterWriteLock();
            try
            {
                if (!volumeMappings.ContainsKey(request.Name))
                {
                    string errorMessage = String.Format("Mapping for volume {0} was not found.", request.Name);
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMessage);
                    return new Response(errorMessage);
                }

                if (this.volumeMappings[request.Name].NumberOfMounts != 0)
                {
                    string errorMesg = String.Format("Cannot remove volume {0} since it is in use.", request.Name);
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, errorMesg);
                    return new Response(errorMesg);
                }

                // Remove the mountpoint.
                try
                {
                    string pathMountpoint = this.volumeMappings[request.Name].Mountpoint;
                    if (System.IO.Directory.Exists(pathMountpoint))
                    {
                        System.IO.Directory.Delete(pathMountpoint);
                    }

                    // Remove the metadata file
                    string metadataFilename = Path.Combine(this.serviceContext.CodePackageActivationContext.WorkDirectory, volumesMetadata, request.Name);
                    if (System.IO.File.Exists(metadataFilename))
                    {
                        System.IO.File.Delete(metadataFilename);
                    }
                }
                catch(Exception ex)
                {
                    TraceWriter.WriteWarningWithId(Constants.TraceSource, traceId, String.Format("{0}Unable to cleanup volume artifacts due to exception: {1}", traceId, ex.Message));
                }

                this.volumeMappings.Remove(request.Name);

                return new Response();
            }
            finally
            {
                this.volumeRWLock.ExitWriteLock();
            }

        }

#region Helpers
        bool VolumeExists(string name)
        {
            this.volumeRWLock.EnterReadLock();
            try
            {
                if (volumeMappings.ContainsKey(name))
                {
                    return true;
                }

                return false;
            }
            finally
            {
                this.volumeRWLock.ExitReadLock();
            }
        }

        bool ServiceExists(string servicePartitionID)
        {
            this.serviceRWLock.EnterReadLock();
            try
            {
                return serviceMappings.Element(GetFormattedPartitionId(servicePartitionID)) != null;
            }
            finally
            {
                this.serviceRWLock.ExitReadLock();
            }
        }

        string GetMountpoint(string volumeName)
        {
            // Mountpoint is expected to end with a trailing slash
            return Path.Combine(this.mountPointBase, volumeName) + Path.DirectorySeparatorChar;
        }

        int GetServicePort(string servicePartitionID)
        {
            string traceId = GetTraceId("VolumeMap.GetServicePort");

            this.serviceRWLock.EnterReadLock();
            try
            {
                string formattedServicePartitionID = GetFormattedPartitionId(servicePartitionID);
                int port = 0;
                if (serviceMappings.Element(formattedServicePartitionID) != null)
                {
                    string portToParse = serviceMappings.Element(formattedServicePartitionID).Value;

                    if (Int32.TryParse(portToParse, out port))
                    {
                        TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, String.Format("Service Port being returned for servicePartitionID {0} is {1}", servicePartitionID, port));
                    }
                    else
                    {
                        if (string.IsNullOrEmpty(portToParse))
                        {
                            TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, String.Format("Unable to parse port for servicePartitionID {0} from null", servicePartitionID));
                        }
                        else
                        {
                            TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, String.Format("Unable to parse port for servicePartitionID {0} from {1}", servicePartitionID, portToParse));
                        }
                    }
                }
                else
                {
                    TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, String.Format("Unable to find servicePartitionID:{0}", servicePartitionID));
                }

                return port;
            }
            finally
            {
                this.serviceRWLock.ExitReadLock();
            }
        }

        // Return true for valid sizeDisk
        bool ParseSizeDisk(string sizeDisk, ref string size, ref string sizeType)
        {
            // Length should greater than 2 (e.g "1GB")
            if (sizeDisk.Length <= Constants.sizeTypeLength)
            {
                return false;
            }
            size = sizeDisk.Substring(0, sizeDisk.Length - Constants.sizeTypeLength);
            sizeType = sizeDisk.Substring(sizeDisk.Length - Constants.sizeTypeLength);

            int sizeInt = 0;

            // Check whether size is positive integer
            if (!Int32.TryParse(size, out sizeInt))
            {
                return false;
            }
            else if (sizeInt <= 0)
            {
                return false;
            }

            // Check sizeType
            if (sizeType != "MB" && sizeType != "GB" && sizeType != "TB")
            {
                return false;
            }

            return true;
        }

        // Return true if request is invalid
        private bool InvalidCreateRequest(VolumeCreateRequest request, ref string errorMessage)
        {
            if (volumeMappings.ContainsKey(request.Name))
            {
                errorMessage = "Name already exists";
                return true;
            }

            // Check if the LUID is exist and within the length limitation
            if (!request.Opts.ContainsKey(Constants.LUID))
            {
                errorMessage = $"Required argument {Constants.LUID} not present";
                return true;
            }
            else
            {
                // +1 because '\0' will be appended later
                if (request.Opts[Constants.LUID].Length + 1 > Constants.maxWCharSizeLUID)
                {
                    errorMessage = $"{Constants.LUID} cannot be more than {Constants.maxWCharSizeLUID} in length";
                    return true;
                }
            }

            // Check if ServicePartitionID is exist
            if (!request.Opts.ContainsKey(Constants.ServicePartitionID))
            {
                errorMessage = $"Required argument {Constants.ServicePartitionID} not present";
                return true;
            }
            else
            {
                if (ServiceExists(request.Opts[Constants.ServicePartitionID]) == false)
                {
                    errorMessage = $"{Constants.ServicePartitionID}:{request.Opts[Constants.ServicePartitionID]} is not registered";
                    return true;
                }
            }

            // Check if SizeDisk is exist and valid
            if (!request.Opts.ContainsKey(Constants.SizeDisk))
            {
                errorMessage = $"Required argument {Constants.SizeDisk} not present";
                return true;
            }
            else
            {
                string size = "";
                string sizeType = "";

                if (!ParseSizeDisk(request.Opts[Constants.SizeDisk], ref size, ref sizeType))
                {
                    errorMessage = $"Invalid {Constants.SizeDisk}:{request.Opts[Constants.SizeDisk]}";
                    return true;
                }
            }

            // Check if FileSystem is exist and valid
            if (!request.Opts.ContainsKey(Constants.FileSystem))
            {
                errorMessage = $"Required argument {Constants.FileSystem} not present";
                return true;
            }
            else
            {
                string fileSystem = request.Opts[Constants.FileSystem];

#if DotNetCoreClrLinux
                // For Linux we convert NTFS to EXT4, the correct fix will be in manifest generator.
                if (fileSystem != "NTFS" && fileSystem != "EXT4")
#else
                // Just support NTFS at this moment
                if (fileSystem != "NTFS")
#endif
                {
                    errorMessage = $"Invalid {Constants.FileSystem}:{request.Opts[Constants.FileSystem]}";
                    return true;
                }
            }

            return false;
        }

        // All the field should be set at createVolume step, if so return true
        void ValidateProvisionLUArgs(VolumeEntry volumeEntry, ref string size, ref string sizeType, ref int servicePort)
        {
            if (string.IsNullOrEmpty(volumeEntry.ServicePartitionID))
            {
                throw new ArgumentNullException("ServicePartitionID is NullOrEmpty.");
            }

            if (ServiceExists(volumeEntry.ServicePartitionID) == false)
            {
                throw new InvalidOperationException("No service instance exists to mount the volume.");
            }

            if (string.IsNullOrEmpty(volumeEntry.LUID) ||
                string.IsNullOrEmpty(volumeEntry.SizeDisk) ||
                string.IsNullOrEmpty(volumeEntry.FileSystem))
            {
                throw new ArgumentNullException("Disk information(LUID, SizeDisk, FileSystem) is NullOrEmpty.");
            }

            if (string.IsNullOrEmpty(volumeEntry.Mountpoint))
            {
                throw new ArgumentNullException("Mountpoint is NullOrEmpty.");
            }

            int iPort = GetServicePort(volumeEntry.ServicePartitionID);
            if ( iPort <= 0)
            {
               throw new ArgumentException("Invalid ServicePort specified: "+iPort);
            }
            else
            {
                servicePort = iPort;
            }

            if (!ParseSizeDisk(volumeEntry.SizeDisk, ref size, ref sizeType))
            {
                throw new ArgumentException("Invalid disk size specified: {0}", volumeEntry.SizeDisk);
            }
        }

        bool ProvisionLUDirect(VolumeEntry volumeEntry, string traceId, string size, string sizeType, uint servicePort)
        {
            bool fProvisionedLU = false;
            
            // Connect to the driver
            uint lastError = 0;
            IntPtr hDriver = ConnectToDevice(ref lastError);
            if (hDriver != INVALID_HANDLE_VALUE)
            {
                try
                {
                    fProvisionedLU = ProvisionLUN(hDriver, volumeEntry.LUID, size, sizeType, volumeEntry.FileSystem, volumeEntry.Mountpoint, servicePort);
                }
                finally
                {
                    // Close the handle to the driver
                    if (!CloseHandle(hDriver))
                    {
                        TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, "Unable to close handle to the driver.");    
                    }
                }
            }
            else
            {
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, String.Format("Unable to provision volume due to error: {0:X}", lastError));
            }
            return fProvisionedLU;
        }

        bool ProvisionLU(string name, VolumeEntry volumeEntry, string traceId)
        {
            bool provisionSuccessful = false;

            // Connect the socket to the remote endpoint. Catch any errors.  
            try
            {
                string size = "";
                string sizeType = "";
                int servicePort = 0;
                this.ValidateProvisionLUArgs(volumeEntry, ref size, ref sizeType, ref servicePort);

                provisionSuccessful = ProvisionLUDirect(volumeEntry, traceId, size, sizeType, (uint)servicePort);
            }
            catch (Exception ex)
            {
                TraceWriter.WriteErrorWithId(Constants.TraceSource, traceId, String.Format("Unable to provision volume due to exception: {0}", ex.Message));
                provisionSuccessful = false;
            }

            return provisionSuccessful;
        }

        //
        // Initialize information about any services and volume mounts.
        //
        private void Initialize()
        {
            string traceId = GetTraceId("VolumeMap.Initialize");

            string servicesMetadataPath = Path.Combine(this.serviceContext.CodePackageActivationContext.WorkDirectory, servicesMetadata);
            Utilities.EnsureFolder(servicesMetadataPath);
            string servicesMetadataFile = Path.Combine(servicesMetadataPath, servicesMetadataFilenameWithExt);
            if (File.Exists(servicesMetadataFile))
            {
                this.serviceRWLock.EnterWriteLock();
                try
                {
                    XElement xElementMap = XElement.Load(servicesMetadataFile);
                    foreach (XElement el in xElementMap.Elements())
                    {
                        serviceMappings.SetElementValue(el.Name.LocalName, el.Value);
                        TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, String.Format("Read serviceMapping for servicePartitionId {0} and Port {1}", el.Name.LocalName, el.Value));
                    }
                }
                finally
                {
                    this.serviceRWLock.ExitWriteLock();
                }
            }
            else
            {
                TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, "No pre-existing Service metadata mappings found.");
            }

            string volumesPath = Path.Combine(this.serviceContext.CodePackageActivationContext.WorkDirectory, volumesMetadata);
            Utilities.EnsureFolder(volumesPath);
            var volumes = Directory.GetFiles(volumesPath);

            if (volumes == null || volumes.Length == 0)
            {
                 TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, "No pre-existing Volume metadata mappings found.");
            }

            foreach (var metadataFile in volumes)
            {
                VolumeCreateRequest volume;
                string mountPoint;
                if (ReadMetadataFile(metadataFile, out volume, out mountPoint))
                {
                    this.volumeMappings[volume.Name] = new VolumeEntry()
                    {
                        VolumeOptions = volume.Opts,
                        ServicePartitionID = volume.Opts[Constants.ServicePartitionID],
                        LUID = volume.Opts[Constants.LUID],
                        SizeDisk = volume.Opts[Constants.SizeDisk],
#if DotNetCoreClrLinux
                        FileSystem  = "EXT4",
#else
                        FileSystem = volume.Opts[Constants.FileSystem],
#endif
                        Mountpoint = mountPoint
                    };
                    VolumeEntry volumeEntry = this.volumeMappings[volume.Name];
                    TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, 
                        String.Format("Read volumeMapping for Volume:{0}, servicePartitionId:{1}, LUID:{2}, sizeDisk:{3}, fileSystem:{4}, mountPoint:{5}", volume.Name, volumeEntry.ServicePartitionID, volumeEntry.LUID, volumeEntry.SizeDisk, volumeEntry.FileSystem, volumeEntry.Mountpoint));
                }
            }
            
            TraceWriter.WriteInfoWithId(Constants.TraceSource, traceId, "Volume map initialized.");
        }

        private bool ReadMetadataFile(string filename, out VolumeCreateRequest volume, out string mountPoint)
        {
            mountPoint = null;
            volume = null;

            try
            {
                using (StreamReader reader = File.OpenText(filename))
                {
                    JsonSerializer serializer = new JsonSerializer();
                    volume = (VolumeCreateRequest) serializer.Deserialize(reader, typeof(VolumeCreateRequest));
                    mountPoint = GetMountpoint(volume.Name);
                }
            }
            catch (Exception e)
            {
                TraceWriter.WriteErrorWithId(Constants.TraceSource, GetTraceId("ReadMetadataFile"), String.Format("Reading metadata file, {0}, failed with exception {1}.", filename, e.Message));
                return false;
            }

            return true;
        }

        private bool WriteMetadataFile(string filename, VolumeCreateRequest volume)
        {
            try
            {
                using (StreamWriter writer = File.CreateText(filename))
                {
                    JsonSerializer serializer = new JsonSerializer();
                    serializer.Serialize(writer, volume);
                }
            }
            catch (Exception e)
            {
                TraceWriter.WriteErrorWithId(Constants.TraceSource, GetTraceId("WriteMetadataFile"), String.Format("Writing metadata file, {0}, failed with exception {1} for volume {2}.", filename, e.Message, volume.Name));
                return false;
            }

            return true;
        }

#endregion

#region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    // TODO: dispose managed state (managed objects).
                    this.volumeRWLock.Dispose();
                    this.serviceRWLock.Dispose();
                }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                // TODO: set large fields to null.

                disposedValue = true;
            }
        }

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        // ~VolumeMap() {
        //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        //   Dispose(false);
        // }

        // This code added to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            // TODO: uncomment the following line if the finalizer is overridden above.
            // GC.SuppressFinalize(this);
        }
#endregion
    }
}
